Skip to content

X-Trace-ID and structured logging output.#1682

Open
MikeNeilson wants to merge 9 commits intodevelopfrom
feature/1666-trace-id
Open

X-Trace-ID and structured logging output.#1682
MikeNeilson wants to merge 9 commits intodevelopfrom
feature/1666-trace-id

Conversation

@MikeNeilson
Copy link
Copy Markdown
Contributor

@MikeNeilson MikeNeilson commented Apr 8, 2026

Okay, somewhat got away from me, but it all works now.

The combination of slf4j and logback, while not required, does simplify the creation of structured logs. As well as having the same log output as OpenDCS uses in cloud environment which will simplify log searches (we aren't learning two formats.)

There is a gRPC context mechanism provided by flogger but I'm still reading up. I think I'm likely to create a custom ContextDataProvider (what Flogger needs implemented) to handle mapping to the SLF4j MDC. so still using that system, just cleaner.

Additionally, since we know we have async things, we needed to sort out how the nested contexts work on a thread pool to make sure the Trace ID passing along correctly.

For those that do not like the log formatting, one can always setup your local environment to use something you find more friendly. However, the cda-plain.log was thrown together at the last moment so suggestions welcome.

The structured output uses jsonl as the extension. At least in VS Code this validates and renders correctly as a defined JSON file... e.g. nice element coloring.

@MikeNeilson MikeNeilson force-pushed the feature/1666-trace-id branch from 558997c to a953cac Compare April 13, 2026 16:37
@MikeNeilson MikeNeilson marked this pull request as ready for review April 13, 2026 16:53
@MikeNeilson
Copy link
Copy Markdown
Contributor Author

NOTE: I'm not hard set on the whole logback thing, it was a rather obnoxious pain to get working; However, I believe there is going to be custom work no matter what as most items that use JUL don't really provide much support for things like the MappedDiagnosticContext concept.

Copy link
Copy Markdown
Collaborator

@krowvin krowvin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tomecat is def a fix, the rest is up to you!

if (UUID_MATCHER.matcher(id).matches()) {
return id;
} else {
throw new IOException("Trace id '" + id + "' is not a valid UUIDish value.");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing here will likely bubble up as a 500. This makes it look like a server issue but really it's for bad client input.

Should we fallback to a generated ID or return a 400 instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but would have to sort out how to do that. Probably need to hijack the response... which is fine since why process any more if the request is invalid.

public static final String CONTEXT_TRACE_ID = "traceID";
public static final String HEADER_TRACE_ID = "X-Trace-ID";

public static final Pattern UUID_MATCHER = Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it will only cover lowercase UUID?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, spec says should be lower case, but to accept both.

Comment thread Dockerfile Outdated
// Just reset back the MDC map to whatever it was before we decided to muck with it.
return () -> {
MDC.clear();
MDC.setContextMap(currentMdc);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if currentMdc is null will it throw an unhandled error? Might need a guard clause for null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MDC::setContextMap handles null inputs since several version earlier than we're using.

final var meta = this.getMetadata();
final var currentMdc = MDC.getCopyOfContextMap();

atCreationMdc.putAll(atCreationMdc);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this putting the current value back into itself?

Is that the same as doing this

myVar = 1

myVar = myVar

print(myVar)

1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... kinda, but not.

It does look super weird, but we have to copy the current context as it is in the thread where install was called, then add all of the values this LoggingContext already has, and then put them onto the current thread.

MDC values are "ThreadLocal" hence all the magic to shift the contexts around.

Copy link
Copy Markdown
Contributor Author

@MikeNeilson MikeNeilson Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit more, The CdaLoggingContext instance was (possibly) created on a different thread than where install() is called. If they're on the same thread, yeah it's basically a no-op, but it's done this way to cover the case it was made for, separate threads.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a little too clever. I think rewriting to:

            final Map<String, String> mergedMdc = new HashMap<>();
            if (currentMdc != null) {
                mergedMdc.putAll(currentMdc);
            }
            mergedMdc.putAll(atCreationMdc);

and then populating a mergedMdc in the two loops followed by

  MDC.setContextMap(mergedMdc);

would accomplish the same thing, be less confusing to look at, and not muck with atCreationMdc (which by its name, shouldn't be modified).

@krowvin
Copy link
Copy Markdown
Collaborator

krowvin commented Apr 14, 2026

This would end up needing merged with #1428 - i'm happy to merge this into 1428 after you get this into main though!

Co-authored-by: Charles Graham <charles.r.graham@usace.army.mil>
Copy link
Copy Markdown
Contributor Author

@MikeNeilson MikeNeilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that Dockerfile change was probably why a specific form of override wasn't working.

public static final String CONTEXT_TRACE_ID = "traceID";
public static final String HEADER_TRACE_ID = "X-Trace-ID";

public static final Pattern UUID_MATCHER = Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, spec says should be lower case, but to accept both.

if (UUID_MATCHER.matcher(id).matches()) {
return id;
} else {
throw new IOException("Trace id '" + id + "' is not a valid UUIDish value.");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but would have to sort out how to do that. Probably need to hijack the response... which is fine since why process any more if the request is invalid.

final var meta = this.getMetadata();
final var currentMdc = MDC.getCopyOfContextMap();

atCreationMdc.putAll(atCreationMdc);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... kinda, but not.

It does look super weird, but we have to copy the current context as it is in the thread where install was called, then add all of the values this LoggingContext already has, and then put them onto the current thread.

MDC values are "ThreadLocal" hence all the magic to shift the contexts around.

// Just reset back the MDC map to whatever it was before we decided to muck with it.
return () -> {
MDC.clear();
MDC.setContextMap(currentMdc);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MDC::setContextMap handles null inputs since several version earlier than we're using.

Comment thread Dockerfile Outdated
@MikeNeilson MikeNeilson changed the title Initial trace id using a bit of a hack. X-Trace-ID and structured logging output. Apr 14, 2026
@MikeNeilson
Copy link
Copy Markdown
Contributor Author

This would end up needing merged with #1428 - i'm happy to merge this into 1428 after you get this into main though!

That is separate, that's more about the transparent proxy, not something within CDA itself.

Comment thread cda-gui/package-lock.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity, why did this get modified for this PR?

final var tags = this.getTags();
if (tags != null) {
tags.asMap().forEach((key, entry) ->
MDC.put(key, String.join(",", entry.stream()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're already streaming, use Collectors.joining(",") in the collect instead of joining the end product.

final var meta = this.getMetadata();
final var currentMdc = MDC.getCopyOfContextMap();

atCreationMdc.putAll(atCreationMdc);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a little too clever. I think rewriting to:

            final Map<String, String> mergedMdc = new HashMap<>();
            if (currentMdc != null) {
                mergedMdc.putAll(currentMdc);
            }
            mergedMdc.putAll(atCreationMdc);

and then populating a mergedMdc in the two loops followed by

  MDC.setContextMap(mergedMdc);

would accomplish the same thing, be less confusing to look at, and not muck with atCreationMdc (which by its name, shouldn't be modified).

public class TraceIdFilter implements Filter
{
public static final String CONTEXT_TRACE_ID = "traceID";
public static final String HEADER_TRACE_ID = "X-Trace-ID";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "X-Trace-ID" instead of (or maybe in addition to?) traceparent: https://www.w3.org/TR/trace-context/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be a 16-byte hex value as standard, not a UUID

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I wasn't aware of that, and from my brief search of what people have done it never came up. Just about everyone just does a UUID with X-Trace-Id or X-Client-Trace-Id.

But I'll go do some more research, all for standards. Especially if used by default in systems like Granafa/etc for searching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants